#include "render_system/gpu_program.h"

#include <memory>
#include <fstream>
#include <exception>
#include <boost/lexical_cast.hpp>

#include "render_system/gpu_info.h"
#include "render_system/gl_header.h"
#include "utility.h"

#pragma warning(disable : 4244)

GPUProgram::GPUProgram(void)
    : _programID(0)
{
}


GPUProgram::~GPUProgram(void)
{
    _clean();
}


void GPUProgram::addShaderFile(const ShaderType type, const std::string& file_path)
{
    GLuint shaderID = 0;
    try
    {
        // open shader source file
        std::ifstream file(file_path, std::ios::in | std::ios::binary);
        if (!file.good())
        {
            std::string err = "fail to open source file: " + file_path + ".";
            throw std::exception(err.c_str());
        }
        // get the source file size
        std::ifstream::pos_type file_size = file.seekg(0, std::ios::end).tellg();
        if (-1 == file_size)
        {
            std::string err = "source file: " + file_path + " is empty.";
            throw std::exception(err.c_str());
        }
        file.seekg(0, std::ios::beg);   // reset the file pos at the begining

        std::unique_ptr<char[]> p_source_code(new char[file_size+std::ifstream::pos_type(1)]);
        file.read(p_source_code.get(), file_size);
        p_source_code[file_size] = '\0';
        file.close();

        // compile shader source code
        // step 1. create a shader handle in OpenGL
        std::string errcode = GpuInfo::getError();

        std::string shaderName;
        switch(type)
        {
        case Vertex:
            shaderID = glCreateShader(GL_VERTEX_SHADER);
            shaderName = "Vertex";
            break;
        case Geometry:
            shaderID = glCreateShader(GL_GEOMETRY_SHADER);
            shaderName = "Geometry";
            break;
        case Fragment:
            shaderID = glCreateShader(GL_FRAGMENT_SHADER);
            shaderName = "Fragment";
            break;
        }

        if (0 == shaderID)
        {
            std::string err = "fail to create " + shaderName + " shader.";
            throw std::exception(err.c_str());
        }

        // step 2. set shader source to the queried shader ID.
        const char* src[] = {p_source_code.get()};
        glShaderSource(shaderID, 1, src, NULL);

        errcode = GpuInfo::getError();
        if ("GL_NO_ERROR" != errcode)
        {
            std::string err = "fail to set " + shaderName + " shader source to shader ID: "
                + boost::lexical_cast<std::string>(shaderID) + " , error code: " + errcode + " .";
            throw std::exception(err.c_str());
        }

        // step 3. compile shader.
        glCompileShader(shaderID);

        // get compile status
        GLint iState = 0;
        glGetShaderiv(shaderID, GL_COMPILE_STATUS, &iState);

        if (GL_TRUE != iState)
        {
            GLint iInfoLogSize = 0;
            glGetShaderiv(shaderID, GL_INFO_LOG_LENGTH, &iInfoLogSize);
            if (1 < iInfoLogSize)
            {
                std::unique_ptr<GLchar[]> pErrorInfo(new GLchar[iInfoLogSize]);
                glGetShaderInfoLog(shaderID, iInfoLogSize, NULL, pErrorInfo.get());

                std::string err = shaderName + " shader compile error:\n" + pErrorInfo.get();
                throw std::exception(err.c_str());
            }
        }
        _shaders.push_back(shaderID);
    }
    catch(const std::exception& e)
    {
        if (0 != shaderID)
        {
            glDeleteShader(shaderID);
            shaderID = 0;
        }
        Log::error(e.what());
        throw;
    }
}


void GPUProgram::use()
{
    if(0 == _programID)
    {
        _create();
        _getActiveAttributes();
        _getActiveUniforms();
    }
    glUseProgram(_programID);
}


void GPUProgram::unuse()
{
    if (0 != _programID)
    {
        glUseProgram(0);
    }
}


void GPUProgram::printActiveAttributes()
{
    printf(Green, "Active Attributes\n");
    for (auto it = _activeAttributesMap.begin(); it != _activeAttributesMap.end(); it++)
    {
        printf("location:%3d, \tattribute: %s\n", it->second, it->first.c_str());
    }
}


void GPUProgram::printActiveUniforms()
{
    printf(Green, "Active Uniforms\n");
    for (auto it = _activeUniformsMap.begin(); it != _activeUniformsMap.end(); it++)
    {
        printf("location:%3d, \tuniform: %s\n", it->second, it->first.c_str());
    }
}


void GPUProgram::setUniform(const std::string& name, const int val)
{
    try
    {
        glUniform1i(_activeUniformsMap.at(name), val);
    }
    catch(...)
    {
        std::string err("fail to set uniform name: ");
        err += name + " .";
        Log::warning(err.c_str());
    }
}


void GPUProgram::setUniform(const std::string& name, const float val)
{
    try
    {
        glUniform1f(_activeUniformsMap.at(name), val);
    }
    catch(...)
    {
        std::string err("fail to set uniform name: ");
        err += name + " .";
        Log::warning(err.c_str());
    }
}

void GPUProgram::setUniform(const std::string& name, const glm::vec2& vec)
{
    try
    {
        glUniform2fv(_activeUniformsMap.at(name), 1, &vec[0]);
    }
    catch(...)
    {
        std::string err("fail to set uniform name: ");
        err += name + " .";
        Log::warning(err.c_str());
    }
}


void GPUProgram::setUniform(const std::string& name, const glm::vec3& vec)
{
    try
    {
        glUniform3fv(_activeUniformsMap.at(name), 1, &vec[0]);
    }
    catch(...)
    {
        std::string err("fail to set uniform name: ");
        err += name + " .";
        Log::warning(err.c_str());
    }
}


void GPUProgram::setUniform(const std::string& name, const glm::vec4& vec)
{
    try
    {
        glUniform4fv(_activeUniformsMap.at(name), 1, &vec[0]);
    }
    catch(...)
    {
        std::string err("fail to set uniform name: ");
        err += name + " .";
        Log::warning(err.c_str());
    }
}


void GPUProgram::setUniform(const std::string& name, const glm::mat4& mat)
{
    try
    {
        glUniformMatrix4fv(_activeUniformsMap.at(name), 1, GL_FALSE, &mat[0][0]);
    }
    catch(...)
    {
        std::string err("fail to set uniform name: ");
        err += name + " .";
        Log::warning(err.c_str());
    }
}


void GPUProgram::_create()
{
    try
    {
        if (_shaders.empty())
        {
            throw std::exception("there is no shader for creating a program.");
        }

        GLint iState = 0;

        _programID = glCreateProgram();
        if(0 == _programID)
        {
            throw std::exception("Error creating program object.");
        }

        // attach shader
        for (auto it = _shaders.begin(); it != _shaders.end(); it++)
        {
            glAttachShader(_programID, *it);
        }

        // Link the program
        glLinkProgram(_programID);
        // get information string's size in byte, include null character.
        glGetProgramiv(_programID, GL_LINK_STATUS, &iState);
        if (GL_TRUE != iState)
        {
            GLint iInfoLogSize = 0;
            glGetProgramiv(_programID, GL_INFO_LOG_LENGTH, &iInfoLogSize);

            if (1 < iInfoLogSize)
            {
                std::unique_ptr<GLchar[]> pErrorInfo(new GLchar[iInfoLogSize]);
                glGetProgramInfoLog(_programID, iInfoLogSize, NULL, pErrorInfo.get());
                throw std::exception(pErrorInfo.get());
            }
        }

        // validate the program
        glValidateProgram(_programID);
        glGetProgramiv(_programID, GL_VALIDATE_STATUS, &iState);
        if (GL_TRUE != iState)
        {
            throw std::exception("shader program validate failed.");
        }

        // clear shaders
        for(auto it = _shaders.begin(); it != _shaders.end(); it++)
        {
            glDetachShader(_programID, *it);
            glDeleteShader(*it);
        }
        _shaders.clear();
    }
    catch(const std::exception& e)
    {
        if (0 != _programID)
        {
            glDeleteProgram(_programID);
            _programID = 0;
        }
        Log::error(e.what());
        throw;
    }
}


void GPUProgram::_getActiveAttributes()
{
    GLint maxLength, nAttribs;
    glGetProgramiv(_programID, GL_ACTIVE_ATTRIBUTES, &nAttribs);
    glGetProgramiv(_programID, GL_ACTIVE_ATTRIBUTE_MAX_LENGTH, &maxLength);

    std::unique_ptr<GLchar[]> name(new GLchar[maxLength]);
    GLchar* pName = name.get();

    GLint written, size, location;
    GLenum type;
    for( int i = 0; i < nAttribs; i++ )
    {
        glGetActiveAttrib(_programID, i, maxLength, &written, &size, &type, pName);
        location = glGetAttribLocation(_programID, pName);
        _activeAttributesMap[pName] = location;
    }
}


void GPUProgram::_getActiveUniforms()
{
    GLint maxLength, nUniforms;
    glGetProgramiv(_programID, GL_ACTIVE_UNIFORMS, &nUniforms);
    glGetProgramiv(_programID, GL_ACTIVE_UNIFORM_MAX_LENGTH, &maxLength);

    std::unique_ptr<GLchar[]> name(new GLchar[maxLength]);
    GLchar* pName = name.get();

    GLint written, size, location;
    GLenum type;
    for( int i = 0; i < nUniforms; ++i )
    {
        glGetActiveUniform(_programID, i, maxLength, &written, &size, &type, pName);
        location = glGetUniformLocation(_programID, pName);
        _activeUniformsMap[pName] = location;
    }
}


void GPUProgram::_clean()
{
    try
    {
        unuse();

        if (0 != _programID)
        {
            glDeleteProgram(_programID);
            _programID = 0;
        }

        if (!_shaders.empty())
        {
            std::for_each(_shaders.begin(), _shaders.end(), glDeleteShader);
            _shaders.clear();
        }

        _activeAttributesMap.clear();
    }
    catch(const std::exception& e)
    {
        Log::error(e.what());
    }
}

int GPUProgram::getActiveAttribute(const std::string& name) const
{
    try
    {
        return _activeAttributesMap.at(name);
    }
    catch(std::out_of_range& /*e*/)
    {
        std::string err("there is no active attribute named: ");
        err += name + " .";
        Log::warning(err.c_str());
//        throw std::exception(err.c_str());
        return -1;
    }
}

int GPUProgram::getActiveUniform( const std::string& name ) const
{
    try
    {
        return _activeUniformsMap.at(name);
    }
    catch(std::out_of_range& /*e*/)
    {
        std::string err("there is no active uniform named: ");
        err += name + " .";
        Log::warning(err.c_str());
        //        throw std::exception(err.c_str());
        return -1;
    }
}
